import os
import struct
import Image

import DataExtraction.BSAHeaderCFA
import DataExtraction.BSAIndex
import DataExtraction.GlobalBSA
import Utils
import Utils.gifmaker

class BSACFA:
    '''Decodes a creature animation.  This will take the form of several frames
    packed into a single buffer as a slideshow sequence.  The frames are
    packed, without any extra data between them.
 
    This function takes an array of frame buffers.  The images encoded in the
    CFA are decoded, then copied one at a time into the array of frame buffers.
 
    The CFA format is subjected to several levels of encoding.
     1) First off, the base palettized image is palettized again.  A look-up
        table is stored at the beginning of the animation, containing only
        those color indices that are used by the animation.
     2) Each re-palettized index value is packed (muxed) into as few bits as
        possible (e.g., if there are only 7 distinct colors, each encoded
        value is stored in 3 bits).  The bit data is packed up to 8-bit
        alignment, so eight 3-bit values will be packed into 24 bits, filling
        3 bytes.  If the image width is not a multiple of this value, extra
        data is stored after the end of the line to pad it to the appropriate
        multiple of bytes.  This is why the CFA header distinguishes between
        the image's compressed and uncompressed widths.
     3) The resulting image data is subjected to run-length encoding.  Since
        all CFAs contain a substantial amount of transparent black, this gives
        most of the compression.
'''
    def __init__(self, entry = None, global_bsa_data = None, default_palette_red_first = None):
                 #global_bsa = None, bsa_index = None, index = 0,
                 #width = 0, height = 0, pitch = 0, frame_count = 0):
        self._default_palette_red_first = default_palette_red_first

        self._header = DataExtraction.BSAHeaderCFA.BSAHeaderCFA()

        self._offset = entry.get_offset()
        self._size = entry.get_size()
        self._global_bsa_data = global_bsa_data
        self._fname = entry.get_name()
        
    # getters
    def get_header(self):
        return self._header
    
    def get_offset(self):
        return self._offset
    
    def get_size(self):
        return self._size
    
    def get_global_bsa_data(self):
        return self._global_bsa_data
    
    def get_filename(self):
        return self._fname
            
    def get_frames(self):
        return self._frames
    
    # setters
    def set_header(self, header):
        self._header = header
        
    def set_offset(self, offset):
        self._offset = offset
        
    def set_size(self, size):
        self._size = size
        
    def set_global_bsa_data(self, global_bsa_data):
        self._global_bsa_data = global_bsa_data
    
    def set_filename(self, filename):
        self._fname = filename
    
    def set_frames(self, frames):
        self._frames = frames

    def load(self):
        # load the header
        self._header.load(self._global_bsa_data, self._offset)
        
        self._pitch = self._header.get_width_uncompressed()

        self._frames = [ [ 0 for x in xrange(self._header.get_width_uncompressed() * \
                                             self._header.get_height()) ]
                                    for frm in xrange(self._header.get_frame_count()) ]
        
        # Get the address where the look-up conversion table is located.  This is
        # used to map the packed color values back to useful palette values.
        remap_offset = self._offset + 0x4c
        
        old_offset = self._global_bsa_data.tell()
        
        self._global_bsa_data.seek(remap_offset, os.SEEK_SET)
        
        r = self._global_bsa_data.read(self._size)
        
        unpack_fmt = str(self._size) + 'B'
        remap = struct.unpack(unpack_fmt, r)
        
        # Get the address where RLE-compressed data starts.  This will include the
        # remapping data stored at 0x4C.
        rle_offset = self._offset + self._header.get_header_size()
        
        # Create a line buffer on the stack.  Generously over-allocate it, since a
        # few extra bytes may be required while demuxing.
        # Zero out the buffer.  Demuxing may require a few extra bytes, but as
        # long as they are all zeroes, the end result will have the demuxing
        # returning zeroes (transparent black) for the extra pixels.
        #encoded = [0 for i in xrange(self._header.get_width_uncompressed() + 16)]
        encoded = []
        
        # Demuxing will return 2-8 index values per pass.  We'll store them
        # here while translating them into actual color values.
        #translate = [0 for i in xrange(8)]
        
        # Allocate a worst-case buffer to hold the de-RLE'd data, since we need
        # to deal with padding for demux alignment.
        # TODO: use a list with extend?
        #decomp = [0 for i in xrange(self._header.get_width_compressed() * self._height *
        #                            self._header.get_frame_count())]

        # The first step is to uncompress the run-length encoded data.  Once
        # this is done, the buffer will contain the bit-packed data, and will
        # also have a predictable size, so we can compute the start of each
        # packed line using Header.WidthCompressed.
        #
        # get the rle_encoded data
        #rle = self._global_bsa_data[rle_offset:self._header.get_width_compressed() * 
        #                       self._header.get_height() * self._header.get_frame_count()]
        
        self._global_bsa_data.seek(rle_offset, os.SEEK_SET)
        
        sz = self._header.get_width_compressed() * self._header.get_height() * \
                    self._header.get_frame_count()
        
        r = self._global_bsa_data.read(sz)
        
        unpack_fmt = str(sz) + 'B'
        
        rle = struct.unpack(unpack_fmt, r)

        # decode it
        decomp, _ = Utils.Utils.rle_decode(rle)
        
        # Byte offset into the bit-packed data to the beginning of the requested
        # frame.  Since the frames are packed one after another, we can iterate
        # over all of them without needing to update the offset between frames.
        offset = 0

        width_unc = self._header.get_width_uncompressed()
        width_comp = self._header.get_width_compressed()
        bpp = self._header.get_bits_per_pixel()
        
        for frame in xrange(self._header.get_frame_count()):
            pOut = 0
            # Data needs to be decoded one line at a time.  The pixel samples are
            # normally packed into smaller byte groupings (e.g., 4 pixels packed into
            # 3 bytes), however, the uncompressed data may not be a multiple of the
            # packing size (for example, a width of 17).  So each line needs to be
            # decoded independently to avoid letting data wrap around and corrupt
            # other lines.  The extra space was already padded with zeroes, so when
            # we demux the packed bits, the extra samples also turn into zeroes.
            
            for y in xrange(self._header.get_height()):
                count = self._header.get_width_uncompressed()
                
                # Copy the line of data to a scratch buffer.  The extra space in the
                # line buffer has been padded with zeroes, which will prevent errors
                # from occurring when demuxing the last pixels on the line (which is
                # only relevant if the line length does not divide evenly with the
                # bytes-per-muxed grouping -- 3 for 6-bit, 5 for 5-bit).
                
                encoded[:] = decomp[offset:offset + self._header.get_width_uncompressed()]
                
                # Now, based upon how many bits are used to store each packed pixel,
                # we demux the bits into expanded 8-bit palette indices.  With the
                # exception of 8-bit pixels, we need to translate the demuxed values
                # into color indices using the values stored in the pRemap buffer.

                if bpp == 8:
                    for x in xrange(width_comp):
                        self._frames[frame][pOut + x] = encoded[x]
                elif bpp == 7:
                    # TODO: pythonize this
                    for x in xrange((width_comp + 6) / 7):
                        tmp = encoded[x * 7:]
                        
                        if len(tmp) < 7:
                            tmp.extend([0 for i in range(7 - len(tmp)) ])
                        translate = Utils.Utils.Demux7(tmp)
                        
                        up = min(8, count)
                        count -= up
                        
                        for j in xrange(up):
                            self._frames[frame][pOut + x * 8 + j] = remap[translate[j]]

                elif bpp == 6:
                    # TODO: pythonize this
                    for x in xrange((width_comp + 2) / 3):
                        tmp = encoded[x * 3:]
                        
                        if len(tmp) < 6:
                            tmp.extend([0 for i in range(6 - len(tmp)) ])
                        translate = Utils.Utils.Demux6(tmp)
                        
                        up = min(4, count)
                        count -= up

                        for j in xrange(up):
                            self._frames[frame][pOut + x * 4 + j] = remap[translate[j]]

                elif bpp == 5:
                    # TODO: pythonize this
                    for x in xrange((width_comp + 4) / 5):
                        tmp = encoded[x * 5:]

                        if len(tmp) < 5:
                            tmp.extend([0 for i in range(5 - len(tmp)) ])
                        translate = Utils.Utils.Demux5(tmp)
                        
                        up = min(8, count)
                        count -= up
                        
                        for j in xrange(up):
                            self._frames[frame][pOut + x * 8 + j] = remap[translate[j]]

                elif bpp == 4:
                    # TODO: pythonize this
                    for x in xrange((width_comp + 1) / 2):
                        tmp = encoded[x * 2:]
                        
                        if len(tmp) < 4:
                            tmp.extend([0 for i in range(4 - len(tmp)) ])                       
                        translate = Utils.Utils.Demux4(tmp)
                        
                        up = min(4, count)
                        count -= up
                        
                        for j in xrange(up):
                            self._frames[frame][pOut + x * 4 + j] = remap[translate[j]]

                elif bpp == 3:
                    # TODO: pythonize this
                    for x in xrange((width_comp + 2) / 3):
                        tmp = encoded[x * 3:]
                        
                        if len(tmp) < 3:
                            tmp.extend([0 for i in range(3 - len(tmp)) ])                        
                        translate = Utils.Utils.Demux3(tmp)
                        
                        up = min(8, count)
                        count -= up
                        
                        for j in xrange(up):
                            self._frames[frame][pOut + x * 8 + j] = remap[translate[j]]
                            
                elif bpp == 2:
                    # TODO: pythonize this
                    for x in xrange(width_comp):
                        tmp = encoded[x:]
                        
                        if len(tmp) < 2:
                            tmp.extend([0 for i in range(2 - len(tmp)) ])                        
                        translate = Utils.Utils.Demux2(tmp)
                        
                        up = min(4, count)
                        count -= up
                        
                        for j in xrange(up):
                            self._frames[frame][pOut + x * 4 + j] = remap[translate[j]]

                elif bpp == 1:
                    # TODO: pythonize this
                    for x in xrange(width_comp):
                        tmp = encoded[x:]
                        
                        translate = Utils.Utils.Demux1(tmp)
                        
                        up = min(8, count)
                        count -= up
                        
                        for j in xrange(up):
                            self._frames[frame][pOut + x * 8 + j] = remap[translate[j]]
                         
                # Move the offset to the start of the next compressed line.
                offset += self._header.get_width_compressed()
                pOut += self._pitch

    def dump(self, filename):
        gif_seq = []

        pal = self._default_palette_red_first
        # Special case for ghosts and wraiths.  They are rendered using
        # palette tricks.  The "color values" are actual light index
        # values that are used to darken the background behind them.
        # To make them look reasonable outside the game, replace the low
        # color values with greyscale so they don't look like a riot of
        # colors.

        if self._fname.find("GHOST") == 0 or self._fname.find("WRAITH") == 0:
            for i in xrange(14):
                pal[i + 0] = 130 - (10 * i)
                pal[i + 1] = 130 - (10 * i)
                pal[i + 2] = 130 - (10 * i)
                
        # create some gifs from the frames
        for (index, frame) in enumerate(self._frames):
            gif_image = Image.new('P', ( self._header.get_width_uncompressed(),
                                         self._header.get_height() ) )

            gif_image.putpalette(pal)
            gif_image.format = 'GIF'

            gif_image.putdata(frame)
            
            gif_seq.append(gif_image)
            #print gif_image.format, gif_image.size, gif_image.mode
        
        out_gif = open(filename, 'wb')
        Utils.gifmaker.makedelta(out_gif, gif_seq, 250, pal, self._frames)
        out_gif.close()
        
